package il.technion.cs236369.dal;

import il.technion.cs236369.exceptions.DatabaseException;
import il.technion.cs236369.exceptions.InvalidPropertiesFile;
import il.technion.cs236369.exceptions.NotFoundException;
import il.technion.cs236369.exceptions.NotValidException;
import il.technion.cs236369.interfaces.ICacheEntry;
import il.technion.cs236369.interfaces.ICacheEntryDal;
import il.technion.cs236369.interfaces.ICacheManager;
import il.technion.cs236369.interfaces.IHttpCommunicator;

import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;

/**
 * A singleton CacheManager
 * 
 * @author Gal Lalouche 036933489 sgallal@t2
 * 
 */
class CacheManager implements ICacheManager {
	static final String				IF_MODIFIED_HEADER			= "If-Modified-Since";
	static final String				CACHE_HEADER				= "Cache-Control";
	static final String				NO_STORE_HEADER				= "no-store";
	static final String				FRESHNESS_CONTROL_HEADER	= "Last-Modified";
	static final int				OK							= 200;
	static final int				UNCHANGED					= 304;

	private final ICacheEntryDal	_dal;
	private final IHttpCommunicator	_getter;

	public CacheManager(IHttpCommunicator getter, Properties propertiesFile) throws InvalidPropertiesFile {
		_getter = getter;

		String driver = propertiesFile.getProperty("httproxy.db.driver").replace("\t", "").trim();
		String url = propertiesFile.getProperty("httproxy.db.url").replace("\t", "").trim();
		String dbName = propertiesFile.getProperty("httproxy.db.name").replace("\t", "").trim();
		String tableName = propertiesFile.getProperty("httproxy.db.table").replace("\t", "").trim();
		String userName = propertiesFile.getProperty("httproxy.db.username").replace("\t", "").trim();
		String password = propertiesFile.getProperty("httproxy.db.password").replace("\t", "").trim();
		if (driver == null || url == null || dbName == null || tableName == null || userName == null || password == null || password == null) {
			throw new InvalidPropertiesFile();
		}

		try {
			_dal = new CacheEntryDalImpl(driver, url, dbName, tableName, userName, password);
		} catch (ClassNotFoundException | SQLException e) {
			throw new InvalidPropertiesFile();
		}
	}

	// Check that all header condition apply
	private boolean checkHeaderConditions(HttpResponse response, String URL) throws SQLException {
		boolean cacheHeaders = true;
		for (Header cacheHeader : response.getHeaders(CACHE_HEADER)) {
			if (cacheHeader.getValue().equals(NO_STORE_HEADER)) {
				cacheHeaders = false;
			}
		}

		if (cacheHeaders == false) {
			System.out.println("!! not saving because of headers");
			return false;
		}

		if (response.containsHeader(FRESHNESS_CONTROL_HEADER) == false) {
			System.out.println("!! not saving because missing last modified");
			return false;
		}

		if (URL.length() > ICacheEntryDal.MAX_ALLOWED_LENGTH) {
			System.out.println("!! not saving because URL is too long");
			return false;
		}

		if (response.containsHeader(CHUNKED_RESPONSE_HEADER())) {
			System.out.println("!! not saving because response is chunked");
			return false;
		}

		return true;
	}

	private String CHUNKED_RESPONSE_HEADER() {
		return "Transfer-Encoding";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ICacheManager#store(org.apache.http.HttpResponse)
	 */
	@Override
	public boolean store(String URL, HttpResponse response) throws DatabaseException, IOException {
		if (response.getStatusLine().getStatusCode() != OK) {
			System.out.println("!! not saving because status code is not " + OK);
			return false;
		}

		try {
			if (checkHeaderConditions(response, URL) == false) {
				return false;
			}
			_dal.store(new CacheEntryImpl(URL, response));
			return true;
		} catch (SQLException e) {
			//			e.printStackTrace();
			throw new DatabaseException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ICacheManager#load(java.net.URL)
	 */
	@Override
	public HttpResponse load(HttpRequest request) throws NotFoundException, DatabaseException, NotValidException {
		try {
			ICacheEntry savedEntry = _dal.load(request.getRequestLine().getUri());
			savedEntry = veriftyValidity(savedEntry, request.getFirstHeader("Host").getValue());
			HttpResponse $ = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, null);
			for (Map.Entry<String, String> entry : savedEntry.getHeaders().entrySet()) {
				$.addHeader(entry.getKey(), entry.getValue());
			}
			$.setEntity(new ByteArrayEntity(savedEntry.getBody()));

			return $;
		} catch (SQLException e) {
			throw new DatabaseException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ICacheManager#contains(java.net.URL)
	 */
	@Override
	public boolean contains(HttpRequest request) throws DatabaseException {
		try {
			return _dal.contains(request.getRequestLine().getUri());
		} catch (SQLException e) {
			throw new DatabaseException(e);
		}
	}

	// verifies the validity (freshness) of the saved entry
	// if it not fresh, it will attemp to refresh, and if it fails, a NotValidException will be thrown
	// will return a fresh entry if successful
	private ICacheEntry veriftyValidity(ICacheEntry savedEntry, String hostHeaderValue) throws NotValidException, DatabaseException, SQLException {
		HttpRequest validityRequest = new BasicHttpRequest("GET", savedEntry.getURL().toString(), HttpVersion.HTTP_1_1);
		validityRequest.addHeader("Host", hostHeaderValue);
		for (Entry<String, String> headerEntry : savedEntry.getHeaders().entrySet()) {
			validityRequest.setHeader(headerEntry.getKey(), headerEntry.getValue());
		}

		String lastModified = savedEntry.getHeaders().get(FRESHNESS_CONTROL_HEADER);
		assert lastModified != null;
		validityRequest.setHeader(IF_MODIFIED_HEADER, lastModified);
		validityRequest.addHeader(CACHE_HEADER, "no-cache");
		HttpResponse response = _getter.get(validityRequest);

		int statusResponse = response.getStatusLine().getStatusCode();

		if (statusResponse == UNCHANGED) {
			return savedEntry;
		} else if (statusResponse == OK) {
			try {
				// refresh
				store(savedEntry.getURL(), response);
				return new CacheEntryImpl(savedEntry.getURL(), response);
			} catch (IOException e) {
				// this should never happen, as the entry already has a well formed URL
				e.printStackTrace();
				assert false;
				throw new RuntimeException("shouldn't happen");
			}
		} else {
			_dal.remove(savedEntry.getURL());
			throw new NotValidException(response);
		}
	}

	@Override
	public void clear() throws DatabaseException {
		try {
			_dal.clear();
		} catch (SQLException e) {
			throw new DatabaseException(e);
		}
	}
}
